//
// Created by Administrator on 2021/8/24.
// Target: Store and Access Chapter2 between App and OpenGL and shaders
// 1. How to create buffers and textures that you can use to
// store data that your program can access
// 2. How to get OpenGL to supply the values of your vertex attributes
// automatically
// 3. How to access textures and buffers from your shaders
//


#include "glad/glad.h"
#include "GLFW/glfw3.h"

#include "Common.h"
#include "Shader/ShaderProgram.h"

GLuint VAO;
ShaderProgram SP;

void Init();
void ProcessInputs(GLFWwindow* window);
void Draw();

int main()
{
    // init glfw
    if(!glfwInit())
    {
        ERROR("glfwInit failed!")
        glfwTerminate();
        return -1;
    }

    // set window hint
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
#ifdef MAC_OS
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 1);
#else
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
#endif
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
#ifdef MAC_OS
    glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GLFW_TRUE);
#endif

    // create window
    GLFWwindow* window = glfwCreateWindow(ScreenWidth, ScreenHeight, "OpenGL", nullptr, nullptr);
    if(!window)
    {
        ERROR("glfwCreateWindow failed!")
        glfwTerminate();
        return -1;
    }
    glfwMakeContextCurrent(window);

    // init opengl
    if(!gladLoadGLLoader((GLADloadproc) glfwGetProcAddress))
    {
        ERROR("gladLoadGLLoader failed!")
        glfwDestroyWindow(window);
        glfwTerminate();
        return -1;
    }
    Init();

    // main loop
    while(!glfwWindowShouldClose(window))
    {
        glfwPollEvents();
        ProcessInputs(window);
        Draw();
        glfwSwapBuffers(window);
    }

    // release opengl
    glDeleteVertexArrays(1, &VAO);

    // release glfw
    glfwDestroyWindow(window);
    glfwTerminate();

    return 0;
}

void Init()
{
    // create and compile and link shaders
    auto shaderInfo = ShaderInfo("../../Chapter2/res/vert.glsl", "../../Chapter2/res/frag.glsl");
    SP = ShaderProgram(shaderInfo);

    // create and bind vertex array
    glGenVertexArrays(1, &VAO);
    glBindVertexArray(VAO);

    // store data
    struct Vertex
    {
    public:
        GLfloat x_;
        GLfloat y_;
        GLfloat z_;

        GLfloat r_;
        GLfloat g_;
        GLfloat b_;

        Vertex(GLfloat x, GLfloat y, GLfloat z, GLfloat r, GLfloat g, GLfloat b)
            : x_(x), y_(y), z_(z),
              r_(r), g_(g), b_(b) {}
    };

    Vertex vertices[] = {
            Vertex(-0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.0f),
            Vertex(0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f),
            Vertex(0.0f, 0.5f, 0.0f, 0.0f, 0.0f, 1.0f)
    };

    // create buffer
    GLuint VBO;
    glGenBuffers(1, &VBO);
    // bind buffer
    glBindBuffer(GL_ARRAY_BUFFER, VBO);
    // create buffer storage but dont fill data firstly
    glBufferData(GL_ARRAY_BUFFER, sizeof vertices, nullptr, GL_STATIC_DRAW);
    // this time. there have two command to fill data.
    // 1. glBufferSubData
    glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof vertices, vertices);
    // 2. map buffer to a pointer
    void* ptr = glMapBuffer(GL_ARRAY_BUFFER, GL_WRITE_ONLY);
    memcpy(ptr, vertices, sizeof vertices);
    glUnmapBuffer(GL_ARRAY_BUFFER);

    // configure vertex attributes,
    // so that opengl can supply data for these attributes from buffer automatically
    // attribute 0: position
    glVertexAttribBinding(0, 0);
    glVertexAttribFormat(0, 3, GL_FLOAT, GL_FALSE, 0);
    glBindVertexBuffer(0, VBO, 0, 6 * sizeof(GLfloat));
    glEnableVertexAttribArray(0);
    // attribute 1: color
    glVertexAttribBinding(1, 1);
    glVertexAttribFormat(1, 3, GL_FLOAT, GL_FALSE, 0);
    glBindVertexBuffer(1, VBO, 3 * sizeof(GLfloat), 6 * sizeof(GLfloat));
    glEnableVertexAttribArray(1);

    // build uniform block
    GLuint UBO;
    glGenBuffers(1, &UBO);
    glBindBuffer(GL_UNIFORM_BUFFER, UBO);
    glBufferData(GL_UNIFORM_BUFFER, 4096, nullptr, GL_STATIC_DRAW);
    void* uPtr = glMapBuffer(GL_UNIFORM_BUFFER, GL_WRITE_ONLY);
    GLubyte* buffer = ((GLubyte*)(uPtr));

    // retrieve information
    static const GLchar* names[] = {
            "Transform._Scale",
            "Transform._Translation",
            "Transform._Rotation",
            "Transform._RotationMatrix"
    };
    GLuint indices[4];
    glGetUniformIndices(SP.id(), 4, names, indices);
    GLint offsets[4];
    GLint arrayStride[4];
    GLint matrixStride[4];
    glGetActiveUniformsiv(SP.id(), 4, indices, GL_UNIFORM_OFFSET, offsets);
    glGetActiveUniformsiv(SP.id(), 4, indices, GL_UNIFORM_ARRAY_STRIDE, arrayStride);
    glGetActiveUniformsiv(SP.id(), 4, indices, GL_UNIFORM_MATRIX_STRIDE, matrixStride);
    // scale float
    *((float*)(buffer + offsets[0])) = 1.0f;
    // translation vec3
    ((float*)(buffer + offsets[1]))[0] = 1.0f;
    ((float*)(buffer + offsets[1]))[1] = 2.0f;
    ((float*)(buffer + offsets[1]))[2] = 0.0f;
    // rotation float4[3]
    static const float rotation[3] = {0.0f, 0.0f, 30.0f};
    GLint offset = offsets[2];
    GLint stride = arrayStride[2];
    for(int i = 0; i < 3; ++i)
    {
        *((float*)(buffer + offset)) = rotation[i];
        offset += stride;
    }
    // rotationMatrix mat4
    // column-major
    // first input column
    static const float matrix[] = {
            cosf(30 * DEG_2_RAD), sinf(30 * DEG_2_RAD), 0.0f, 0.0f,
            -sinf(30 * DEG_2_RAD), cosf(30 * DEG_2_RAD), 0.0f, 0.0f,
            0.0f, 0.0f, 0.0f, 0.0f,
            0.0f, 0.0f, 0.0f, 1.0f
    };
    for(int i = 0; i < 4; ++i)
    {
        offset = offsets[3] + matrixStride[3] * i;
        for(int j = 0; j < 4; ++j)
        {
            *((float*)(buffer + offset)) = matrix[i * 4 + j];
            offset += sizeof(GLfloat);
        }
    }

    glUnmapBuffer(GL_UNIFORM_BUFFER);

    //GLuint uniformBlockId = glGetUniformBlockIndex(SP.id(), "Transform");
    //glUniformBlockBinding(SP.id(), uniformBlockId, 2);
    // or specify in shader use a binding qualifier directly(recommend)
    glBindBufferBase(GL_UNIFORM_BUFFER, 2, UBO);
}

void ProcessInputs(GLFWwindow* window)
{

}

void Draw()
{
    static const GLfloat bgColor[] = {0.2f, 0.2f, 0.2f, 1.0f};
    glClearBufferfv(GL_COLOR, 0, bgColor);

    SP.Use();

    glDrawArrays(GL_TRIANGLES, 0, 3);
}